<!DOCTYPE html>
<title>Shadow DOM: Imperative Slot API</title>
<meta name="author" title="Yu Han" href="mailto:yuzhehan@chromium.org">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/shadow-dom.js"></script>

<div id="test_basic">
  <div id="host1"></div>
  <div id="host2"></div>
  <div id="host3"></div>
</div>
<script>
test(() => {
  let tTree = createTestTree(test_basic);
  assert_not_equals(tTree.host1.attachShadow({ mode: 'open', slotAssignment: 'manual'}),
                    null, 'slot assignment manual should work');
  assert_not_equals(tTree.host2.attachShadow({ mode: 'open', slotAssignment: 'name'}),
                    null, 'slot assignment auto should work');
  assert_throws_js(TypeError, () => {
    tTree.host3.attachShadow({ mode: 'open', slotAssignment: 'exceptional' })},
                'others should throw exception');
}, 'attachShadow can take slotAssignment parameter.');
</script>

<div id="test_errors">
  <div id="host1">
    <template data-mode="open" data-slot-assignment="name">
      <slot id="s1"></slot>
    </template>
    <div id="c1"></div>
  </div>
  <div id="host2">
    <template data-mode="open" data-slot-assignment="manual">
      <slot id="s2"></slot>
    </template>
  </div>
  <div id="c2"></div>
</div>
<script>
test(() => {
  let tTree = createTestTree(test_errors);
  assert_array_equals(tTree.s1.assignedElements(), [tTree.c1]);
  assert_equals(tTree.c1.assignedSlot, tTree.s1);

  assert_throws_dom('NotAllowedError', () => { tTree.s1.assign([]); });
  assert_array_equals(tTree.s1.assignedElements(), [tTree.c1]);
  assert_equals(tTree.c1.assignedSlot, tTree.s1);
}, 'Imperative slot API throws exception when not slotAssignment != \'manual\'.');

test(() => {
  let tTree = createTestTree(test_errors);
  assert_throws_dom('NotAllowedError', () => { tTree.s2.assign([tTree.c2]); });

  assert_throws_dom('NotAllowedError', () => { tTree.s2.assign([tTree.host1]); });
}, 'Imperative slot API throws exception when slottable parentNode != slot\'s host.');
</script>

<div id="test_assign">
  <div id="host">
    <template id="shadow_root" data-mode="open" data-slot-assignment="manual">
      <slot id="s1"></slot>
      <slot id="s2"></slot>
      <slot id="s3"></slot>
    </template>
    <div id="c1"></div>
    <div id="c2"></div>
    <div id="c3"></div>
    <div id="nested">
       <div id="ns1"></div>
    </div>
  </div>
  <div id="c4"></div>
  <div id="host4">
    <template id="shadow_root4" data-mode="open" data-slot-assignment="manual">
      <slot id="s4" name="s4"></slot>
    </template>
  </div>
</div>
<script>
test(() => {
  let tTree = createTestTree(test_assign);
  assert_array_equals(tTree.s2.assignedElements(), []);
  assert_equals(tTree.c1.assignedSlot, null);

  tTree.s1.assign([tTree.c1]);
  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1]);
  assert_equals(tTree.c1.assignedSlot, tTree.s1);

  tTree.s2.assign([tTree.c2, tTree.c3]);
  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1]);
  assert_array_equals(tTree.s2.assignedNodes(), [tTree.c2, tTree.c3]);
}, 'Imperative slot API can assign nodes in manual slot assignment.');

test(() => {
  let tTree = createTestTree(test_assign);

  tTree.s1.assign([tTree.c2, tTree.c3, tTree.c1]);
  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c2, tTree.c3, tTree.c1]);
  assert_equals(tTree.c1.assignedSlot, tTree.s1);
  assert_equals(tTree.c2.assignedSlot, tTree.s1);
  assert_equals(tTree.c3.assignedSlot, tTree.s1);

  tTree.s1.assign([tTree.c1, tTree.c2]);
  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1, tTree.c2]);
  assert_equals(tTree.c1.assignedSlot, tTree.s1);
  assert_equals(tTree.c2.assignedSlot, tTree.s1);
  assert_equals(tTree.c3.assignedSlot, null);

  tTree.s1.assign([tTree.c3, tTree.c2, tTree.c1]);
  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c3, tTree.c2, tTree.c1]);
  assert_equals(tTree.c1.assignedSlot, tTree.s1);
  assert_equals(tTree.c2.assignedSlot, tTree.s1);
  assert_equals(tTree.c3.assignedSlot, tTree.s1);
}, 'Order of slottables is preserved in manual slot assignment.');

test(() => {
  let tTree = createTestTree(test_assign);

  tTree.s1.assign([tTree.c2, tTree.c3, tTree.c1]);
  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c2, tTree.c3, tTree.c1]);

  tTree.s2.assign([tTree.c2]);
  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c3, tTree.c1]);
  assert_array_equals(tTree.s2.assignedNodes(), [tTree.c2]);
  assert_equals(tTree.c1.assignedSlot, tTree.s1);
  assert_equals(tTree.c2.assignedSlot, tTree.s2);
  assert_equals(tTree.c3.assignedSlot, tTree.s1);

  tTree.s3.assign([tTree.c3]);
  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1]);
  assert_array_equals(tTree.s2.assignedNodes(), [tTree.c2]);
  assert_array_equals(tTree.s3.assignedNodes(), [tTree.c3]);
  assert_equals(tTree.c1.assignedSlot, tTree.s1);
  assert_equals(tTree.c2.assignedSlot, tTree.s2);
  assert_equals(tTree.c3.assignedSlot, tTree.s3);
}, 'Previously assigned slottable is moved to new slot when it\'s reassigned.');

test(() => {
 let tTree = createTestTree(test_assign);

  tTree.s1.assign([tTree.c1]);
  tTree.s2.assign([tTree.c2, tTree.c1]);
  tTree.s3.assign([tTree.c1, tTree.c3]);

  assert_array_equals(tTree.s1.assignedNodes(), []);
  assert_array_equals(tTree.s2.assignedNodes(), [tTree.c2]);
  assert_array_equals(tTree.s3.assignedNodes(), [tTree.c1, tTree.c3]);
  assert_equals(tTree.c1.assignedSlot, tTree.s3);
  assert_equals(tTree.c2.assignedSlot, tTree.s2);
  assert_equals(tTree.c3.assignedSlot, tTree.s3);
}, 'Order and assignment of nodes are preserved during multiple assignment in a row.');

test(() => {
  let tTree = createTestTree(test_assign);

  // tTree.c4 is invalid for tTree.host slot assignment.
  try {
    tTree.s1.assign([tTree.c1, tTree.c2, tTree.c4]);
    assert_unreached('assign() should have failed) ');
  } catch (err) {
    assert_equals(err.name, 'NotAllowedError');
  }

  assert_array_equals(tTree.s1.assignedNodes(), []);
  assert_equals(tTree.c1.assignedSlot, null);
  assert_equals(tTree.c2.assignedSlot, null);
  assert_equals(tTree.c4.assignedSlot, null);

  tTree.s1.assign([tTree.c2, tTree.c3, tTree.c1]);
  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c2, tTree.c3, tTree.c1]);

  try {
    tTree.s1.assign([tTree.c4]);
    assert_unreached('assign() should have failed) ');
  } catch (err) {
    assert_equals(err.name, 'NotAllowedError');
  }

  // Previous state is preserved.
  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c2, tTree.c3, tTree.c1]);
  assert_equals(tTree.c1.assignedSlot, tTree.s1);
  assert_equals(tTree.c2.assignedSlot, tTree.s1);
  assert_equals(tTree.c3.assignedSlot, tTree.s1);
}, 'Assigning invalid nodes causes exception and slot returns to its previous state.');

test(() => {
  let tTree = createTestTree(test_assign);

  tTree.s1.assign([tTree.c1, tTree.c2, tTree.c3]);
  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1, tTree.c2, tTree.c3]);

  tTree.host4.append(tTree.s1);
  assert_array_equals(tTree.s1.assignedNodes(), []);
}, 'Moving a slot to a new host, the slot loses its previously assigned slottables.');

test(() => {
  let tTree = createTestTree(test_assign);

  tTree.s1.assign([tTree.c1, tTree.c2, tTree.c3]);
  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1, tTree.c2, tTree.c3]);

  tTree.shadow_root.insertBefore(tTree.s2, tTree.s1);
  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1, tTree.c2, tTree.c3]);
  assert_array_equals(tTree.s2.assignedNodes(), []);

  tTree.shadow_root.insertBefore(tTree.s4, tTree.s1);
  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1, tTree.c2, tTree.c3]);
  assert_array_equals(tTree.s4.assignedNodes(), []);

  tTree.ns1.append(tTree.s1);
  assert_array_equals(tTree.s1.assignedNodes(), []);
}, 'Moving a slot\'s tree order position within a shadow host has no impact on its assigned slottables.');

test(() => {
  let tTree = createTestTree(test_assign);

  tTree.s1.assign([tTree.c1, tTree.c2, tTree.c3]);
  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1, tTree.c2, tTree.c3]);

  tTree.host4.append(tTree.c1);
  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c2, tTree.c3]);
  assert_array_equals(tTree.s4.assignedNodes(), []);

  tTree.s4.assign([tTree.c1]);
  assert_array_equals(tTree.s4.assignedNodes(), [tTree.c1]);
  assert_equals(tTree.c1.assignedSlot, tTree.s4);
}, 'Appending slottable to different host, it loses slot assignment. It can be re-assigned within a new host.');

test(() => {
  let tTree = createTestTree(test_assign);

  tTree.s1.assign([tTree.c1]);
  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1]);

  tTree.shadow_root4.insertBefore(tTree.s1, tTree.s4);
  assert_array_equals(tTree.s1.assignedNodes(), []);
  // Don't trigger slot assignment on previous shadow root.
  // assert_array_equals(tTree.s2.assignedNodes(), []);

  tTree.shadow_root.insertBefore(tTree.s1, tTree.s2);
  assert_array_equals(tTree.s1.assignedNodes(), []);
}, 'Previously assigned node should not be assigned if slot moved to a new shadow root. The slot remains empty when moved back, no trigger recalc.');

test(() => {
  let tTree = createTestTree(test_assign);

  tTree.s1.assign([tTree.c1]);
  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1]);

  tTree.shadow_root4.insertBefore(tTree.s1, tTree.s4);
  assert_array_equals(tTree.s1.assignedNodes(), []);
  // Trigger slot assignment on previous shadow root.
  assert_array_equals(tTree.s2.assignedNodes(), []);

  tTree.shadow_root.insertBefore(tTree.s1, tTree.s2);
  assert_array_equals(tTree.s1.assignedNodes(), []);
}, 'Previously assigned node should not be assigned if slot moved to a new shadow root. The slot remains empty when moved back, trigger recalc.');

test(() => {
  let tTree = createTestTree(test_assign);

  tTree.s1.assign([tTree.c1, tTree.c1, tTree.c1]);
  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1]);

  tTree.s1.assign([tTree.c1, tTree.c1, tTree.c2, tTree.c2, tTree.c1]);
  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c2, tTree.c1]);
}, 'Assignment with the same node in parameters should be ignored, last one wins.');

test(() => {
  let tTree = createTestTree(test_assign);

  tTree.s1.assign([tTree.c1, tTree.c2, tTree.c3]);
  tTree.s1.remove();

  assert_equals(tTree.c1.assignedSlot, null);
  assert_equals(tTree.c2.assignedSlot, null);
  assert_equals(tTree.c3.assignedSlot, null);
}, 'Removing a slot from DOM resets its slottable\'s slot assignment.');

test(() => {
  let tTree = createTestTree(test_assign);

  tTree.s1.assign([tTree.c1]);
  tTree.s2.assign([tTree.c2]);
  tTree.s3.assign([tTree.c3]);
  tTree.shadow_root.insertBefore(tTree.s2, tTree.s1);
  tTree.shadow_root.insertBefore(tTree.s3, tTree.s1);

  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1]);
  assert_array_equals(tTree.s2.assignedNodes(), []);
  assert_array_equals(tTree.s3.assignedNodes(), []);
  assert_equals(tTree.c1.assignedSlot, tTree.s1);
  assert_equals(tTree.c2.assignedSlot, null);
  assert_equals(tTree.c3.assignedSlot, null);

  tTree.s2.remove();

  assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1]);
  assert_array_equals(tTree.s3.assignedNodes(), []);
  assert_equals(tTree.c1.assignedSlot, tTree.s1);
  assert_equals(tTree.c2.assignedSlot, null);
  assert_equals(tTree.c3.assignedSlot, null);
}, 'A slot should be cleared of assigned nodes even if it\'s re-inserted into the same shadow root.');
</script>
